import java.util.ArrayList;
import java.util.List;
import com.adventnet.snmp.snmp2.SnmpVarBind;
/**
 * SNMPCrawler crawls the network to discover devices
 * @author skinner@kth.se
 * @author aidi@kth.se
 */
class SNMPCrawler {
	// First IP to start with
	private String startIp;
	
	// Every crawl is associated with a SNMP session
	private SNMPUtils su;
	
	// A list to keep track of all discovered Routers
	private List<Router> routers = new ArrayList<Router>();
	
	// A list of all IP Addresses we have already attempted to find neighbors for
	private List<String> knownIps = new ArrayList<String>();
	
	/**
	 * All the crawler needs to begin is a first IP address
	 * @param ip
	 */
	public SNMPCrawler(String startIp) {
		this.su = new SNMPUtils();
		this.startIp = startIp;
		this.start();
	}
	
	private void start() {
		su.open();
		
		// Contact the only router we know about
		try {
			createRouter(startIp);
		} catch (NullPointerException npe) {
			System.out.println("ERROR: No router found with IP: " + startIp + "\nExiting...");
			npe.printStackTrace();
			System.exit(-1);
		}
		
		// Router list should be non-empty now, so we look through to see if any routers
		//	have any neighbors we haven't contacted yet
		String ip;
		while ((ip = newIp()) != null) {
			createRouter(ip);
		}
		
		su.close();
	}
	
	public String newIp() {
		for(Router r: routers) {
			for(String ip: r.getNeighborIps()) {
				if (!knownIps.contains(ip))
					return ip;
			}
		}
		return null;
	}
	
	public void createRouter(String ip) {
		try {			
			Router r = new Router(su.getVar(ip, SNMPUtils.OIDS.get("sysName")).toString());
			
			// Then add its interfaces
			addInterfaces(r, ip);
			
			// Then add its neighbors
			addNeighbors(r, ip);
			
			// Add this router to our list of known routers
			routers.add(r);
			
			// And add it's IP's, so we don't communicate with it more than once
			knownIps.addAll(r.getLocalIps());
			
			System.out.println("Crawled Router: " + r.getHostname());
			int i = 1;
			for (RouterInf ri: r.getInterfaces().values()) {
				System.out.println("\tInterface ("+ i++ +"):\t" + ri.getIp() + "\t" + ri.getDesc() );
			}
			for (String nip: r.getNeighborIps()) {
				System.out.println("\tNeighbor:\t" + nip);
			}
		} catch (NullPointerException npe) {
			// Throw an error if this is the startIp
			if (ip.equals(startIp)) {
				throw new NullPointerException();
			}
			// Otherwise do nothing.. skip this router if we were not able to get all interfaces or local IPs
			// 	Idea is that, we will ask this router again, when we find another router who has it as a neighbor,
			//	Since this topology is a ring, this should work, and because our while loop will keep asking
		}

	}
	
	/**
	 * A method to lookup and add the neighbors of a router r at IP address ip
	 * @param r
	 * @param ip
	 */
	public void addNeighbors(Router r, String ip) {
		List<String> neighborIps = new ArrayList<String>();
		SnmpVarBind svb = su.getVarBind(ip, SNMPUtils.OIDS.get("ipRouteNextHop"));	
		if (svb != null) {
			// Lookup all IP next hops, and maintain a list of unique IP addresses not on this device
			while (svb.getObjectID().toString().startsWith(SNMPUtils.OIDS.get("ipRouteNextHop"))) {
				String nextHop = svb.getVariable().toString();
				//System.out.println("ipRouteNextHop: " + nextHop);
				if(!neighborIps.contains(nextHop) && !r.getLocalIps().contains(nextHop)) {
					neighborIps.add(nextHop);
				}
				svb = su.getVarBind(ip, svb.getObjectID().toString());
				if (svb == null) {
					System.err.println("Got ONLY " + r.getNeighborIps().size() + " neighbor IPs for Router: " + r.getHostname() + " NULL BREAK");
					break;
				}
			}
		
			// Add neighbors to router
			r.setNeighborIps(neighborIps);
		} else {
			System.err.println("Could not get neighbor IPs for Router: " + r.getHostname() + "@" + ip);
		}
	}
	
	/**
	 * A method to lookup and add the interfaces of a router r at IP address ip
	 * @param r
	 * @param ip
	 */
	public void addInterfaces(Router r, String ip) throws NullPointerException {
		/* Record the index numbers for the interfaces */
		List<Integer> index = new ArrayList<Integer>();
		SnmpVarBind ifIndex = su.getVarBind(ip, SNMPUtils.OIDS.get("ifIndex"));
		if(ifIndex != null) {
			// Seems a bit pointless to go through just to grab an int, but 
			//   just in case it is possible they are out of order
			while (ifIndex.getObjectID().toString().startsWith(SNMPUtils.OIDS.get("ifIndex"))) {
	
					// Create a list of the indices
					index.add((Integer) ifIndex.getVariable().toValue());
					
					// Get next MIB in tree
					ifIndex = su.getVarBind(ip, ifIndex.getObjectID().toString());
					
					if (ifIndex == null) {
						System.err.println("Got ONLY " + index.size() + " interface indices for Router: " + r.getHostname() + " NULL BREAK");
						throw new NullPointerException();
					}
			}
		} else {
			System.err.println("Could not get interface indices for Router: " + r.getHostname() + "@" + ip);
			throw new NullPointerException();
		}

		SnmpVarBind ifDescr = su.getVarBind(ip, SNMPUtils.OIDS.get("ifDescr"));		
		if (ifDescr != null) {
			// Create a new interface for every interface present in the table
			//	This only works if every entry has an index that follows 1,2,3 ...
			//	(i.e. no out of order 1,3,2; no skips 1,3,4) - is this a valid assumption??
			int i = 0;
			while (ifDescr.getObjectID().toString().startsWith(SNMPUtils.OIDS.get("ifDescr"))) {
				// Assign both index (from above) and description at once 
				r.getInterfaces().put(index.get(i++),new RouterInf(ifDescr.getVariable().toString()));
				//r.getInterfaces().add(new RouterInf(ifDescr.getVariable().toString()));
				
				// Get next OID variable binding
				ifDescr = su.getVarBind(ip, ifDescr.getObjectID().toString());
				
				if (ifDescr == null) {
					System.err.println("Got ONLY " + r.getInterfaces().size() + " interfaces for Router: " + r.getHostname() + " NULL BREAK");
					throw new NullPointerException();
				}
			}
		} else {
			System.err.println("Could not get interfaces for Router: " + r.getHostname() + "@" + ip);
			throw new NullPointerException();
		}

		/* Make a list of with ifIndex'd interfaces are in the IP routing table */
		List<Integer> ipIndex = new ArrayList<Integer>();
		SnmpVarBind ipAdEntIfIndex = su.getVarBind(ip, SNMPUtils.OIDS.get("ipAdEntIfIndex"));
		if(ipAdEntIfIndex != null) {
			// Go through these indices, so we can match an IP address to an interface description
			while (ipAdEntIfIndex.getObjectID().toString().startsWith(SNMPUtils.OIDS.get("ipAdEntIfIndex"))) {
					// Create a list of the ipAdEntIfIndex's
					ipIndex.add((Integer) ipAdEntIfIndex.getVariable().toValue());
					
					// Get next MIB in tree
					ipAdEntIfIndex = su.getVarBind(ip, ipAdEntIfIndex.getObjectID().toString());
					if (ipAdEntIfIndex == null) {
						System.err.println("Got ONLY " + ipIndex.size() + " IP indices for Router: " + r.getHostname() + " NULL BREAK");
						throw new NullPointerException();
					}
			}
		} else {
			System.err.println("Could not get IP indices for Router: " + r.getHostname() + "@" + ip);
			throw new NullPointerException();
		}
			
		SnmpVarBind ipAdEntAddr = su.getVarBind(ip, SNMPUtils.OIDS.get("ipAdEntAddr"));
		if (ipAdEntAddr != null) {

			// Then check IP Table to get associated IP addresses
			int i = 0;
			while (ipAdEntAddr.getObjectID().toString().startsWith(SNMPUtils.OIDS.get("ipAdEntAddr"))) {
				r.getInterfaces().get(ipIndex.get(i++)).setIp(ipAdEntAddr.getVariable().toString());
				
				// And add it to localIp list, because it's easier to handle later, 
				//	than having to go through interface list every time
				r.getLocalIps().add(ipAdEntAddr.getVariable().toString());		
	
				// Get next OID variable binding
				ipAdEntAddr = su.getVarBind(ip, ipAdEntAddr.getObjectID().toString());
				if (ipAdEntAddr == null) {
					System.err.println("Got ONLY " + r.getLocalIps().size() + " local IPs for Router: " + r.getHostname() + " NULL BREAK");
					throw new NullPointerException();
				}
			}
		} else {
			System.err.println("Could not get Local IPs for Router: " + r.getHostname() + "@" + ip);
			throw new NullPointerException();
		}
	}


	public List<Router> getRouters() {
		return routers;
	}
	public List<String> getKnownIps() {
		return knownIps;
	}
}
